CMG-1093 | Contentful migration got stuck some time - #1122
Conversation
🔒 Security Scan Results
⏱️ SLA Breach Summary
ℹ️ Vulnerabilities Without Available Fixes (Informational Only)The following vulnerabilities were detected but do not have fixes available (no upgrade or patch). These are excluded from failure thresholds:
✅ BUILD PASSED - All security checks passed |
umesh-more-cstk
left a comment
There was a problem hiding this comment.
COMMENT review - non-gating. The human decides the merge gate.
Verdict: clean, correct bug fix. I traced the root cause and both changes and they hold up.
Verified-good
writeOneFile(line 338): the old callback-basedfs.writeFilewas fire-and-forget, soawait writeOneFile(...)resolved before the bytes hit disk.migration.service.tsawaitscreateAssetsand then immediately runscreateEntry(lines 523 -> 535, and 948), and the entries step readsassets.jsonto resolve asset references - a write/read race that matches the reported "stuck / partial state" symptom. Switching toawait fs.promises.writeFilefixes it.writeOneFileis module-private (not exported), and all three call sites - 361 insidewriteFile's try/catch, and 800 & 814 insidecreateAssets' try/catch - already handle a rejection, so the now-throwing behavior introduces no unhandled rejection.mkdir(assetsSave, { recursive: true })(line 797): necessary and correctly placed. Lines 800 & 814 write viawriteOneFiledirectly, which does NOT mkdir (unlike thewriteFilehelper at line 355 used on line 816).assetsSaveis otherwise only created as a side effect of a successful per-asset write (saveAsset, line 722, on thefiles/<id>subdir). If every asset fails or throws before any success,assetsSavewould not exist and the two directwriteOneFilewrites would ENOENT - exactly what this guards.
Non-blocking
- question - error swallowing after the fix: see inline on line 800.
- nit - no automated test accompanies an IO-ordering fix (author acknowledged this in the checklist). Given the race nature, a test asserting
assets.jsonexists oncecreateAssetsresolves would lock in the fix and prevent regression.
Scope: diff is a single file in api, matching the stated Affected Area and the ticket. No unrelated churn.
🔒 Security Scan Results
⏱️ SLA Breach Summary
ℹ️ Vulnerabilities Without Available Fixes (Informational Only)The following vulnerabilities were detected but do not have fixes available (no upgrade or patch). These are excluded from failure thresholds:
✅ BUILD PASSED - All security checks passed |
umesh-more-cstk
left a comment
There was a problem hiding this comment.
Re-review at head 86c00d7d (COMMENT — non-gating)
Re-reviewed after the new commit 86c00d7d "fix(contentful): rethrow from createAssets so write failures surface" landed on top of the previously-reviewed 0757fd0e. The only change since the last review is the single added line throw err; in the createAssets catch block. The diff is still confined to api/src/services/contentful.service.ts (3 hunks) — scope is clean and matches the api Affected Area.
Status of the three points from the prior review (0757fd0e):
- nit —
writeOneFilenow rejects instead of swallowing. Resolved by design. All three call sites (createAssetsL800/L814 inside itstry, andwriteFileL361 inside its owntry) are covered by acatch, and the newthrow erris exactly what makes awriteOneFilerejection surface. Correct. - question — does downstream
createEntrytolerate a missing/partialassets.jsonafter a failed write? Effectively addressed for the same run:startTestMigration/startMigrationrun the Contentful steps as a sequentialawaitchain, so a throw fromcreateAssetsnow aborts beforecreateTaxonomy/createEntry/createVersionFilerun —createEntryis no longer reached with a half-writtenassets.json. See the blocker below for the caveat on how that abort surfaces. - nit — no accompanying test. Still not addressed; no test file in the diff. Non-blocking (see body note).
Blocking
contentful.service.ts:833— the propagated error lands on a fire-and-forget promise with no handler. The controller invokesmigrationService.startTestMigration(req)/startMigration(req)withoutawaitand without.catch()(api/src/controllers/migration.controller.ts:27and:40), responds200immediately, and the migration runs detached. There is no globalprocess.on('unhandledRejection')anywhere inapiand no--unhandled-rejectionsflag in the start scripts. So the newthrow errturns an asset write/mkdir/read failure into an unhandled promise rejection, which on Node's default (throw, v15+) terminates the whole API process — taking down every in-flight migration — while the client already got a200and never learns it failed. Before this PR,createAssetsswallowed the error and continued. Details/fix options inline.
Non-blocking
- No test (prior nit #3, still open). The author's checklist rationale (race-condition/IO fix) is now weaker: with the
await+throw, the behavior is deterministic and unit-testable (e.g. stubfs.promises.writeFileto reject and assertcreateAssetsrejects; stub it to resolve and assertassets.jsoncontent). A small test would lock in the fix.
Verified good
writeOneFileawait fs.promises.writeFile(L338) — the previous callback form genuinely resolved the outerawaitbefore the write completed; this is a real fix.mkdir(assetsSave, { recursive: true })(L797) — necessary and correct: when every asset fails,saveAssetnever creates a dir underassetsSave(it onlymkdirsassetPathon the success path), so the subsequentwriteOneFile(assets.json)wouldENOENTwithout this.recursivemakes it idempotent for the normal path.- The failure is still logged via
customLoggerimmediately before the throw, so it is not silent in the logs — the gap is purely the missing top-level handler.
Out-of-diff notes (can't anchor inline)
- The fire-and-forget controller (
migration.controller.ts:27/:40) and the absence of a global rejection handler are pre-existing and systemic —aem.service.tscreateAssetsalready propagates (no outer try/catch), so this PR brings Contentful in line with that pattern rather than inventing it. That's why the recommended fix likely belongs instartTestMigration/startMigration(wrap the switch, mark the project failed, log) or in the controller, not necessarily in this file.
🔒 Security Scan Results
⏱️ SLA Breach Summary
ℹ️ Vulnerabilities Without Available Fixes (Informational Only)The following vulnerabilities were detected but do not have fixes available (no upgrade or patch). These are excluded from failure thresholds:
✅ BUILD PASSED - All security checks passed |
🔗 Jira Ticket
CMG-1093 — Delta migration | v1.0.0 | Contentful migration got stuck some time
📋 PR Type
📝 Description
What changed?
writeOneFilenow usesfs.promises.writeFileand is properly awaited. It was previously calling the callback-basedfs.writeFilein a fire-and-forget pattern, so the outerawait writeOneFile(...)returned before the file was actually written to disk.createAssets, addedfs.promises.mkdir(assetsSave, { recursive: true })before writing the assets schema/failed files, so the destination directory is guaranteed to exist.Why?
During delta Contentful migrations the process would occasionally hang or leave a partial state. Root cause: the assets step kicked off a non-awaited write and moved on, and if the
assetsSavedirectory did not yet exist the callback error was swallowed viaconsole.error, leaving downstream steps waiting on a file that was never written. Both issues are fixed here.🧩 Affected Areas
api— Node.js backendui— React frontendupload-api— Upload API serverdocker/docker-compose🧪 How to Test
assets/output directory as the assets step runs.assets/<project>/assets.json(and the failed-assets file, if any) are written before the next step begins.Expected result: Migration completes end-to-end; the assets directory is created if missing,
assets.jsonis fully written before subsequent steps run, and noError writing file: 3messages appear in logs.📸 Screenshots / Recordings
N/A — backend-only fix, no UI changes.
🔗 Related PRs / Dependencies
✅ Author Checklist
bugfix/cmg-1093-contentful-migration-issue.env/example.envupdated if new environment variables were added — N/Anpm test)README.md/ docs updated if behaviour changed — N/A👀 Reviewer Notes
Please focus on:
writeOneFilechange — confirm no other caller relies on the previous non-awaiting behaviour (grepshows all call sites alreadyawaitit).mkdirplacement increateAssets— it runs afterPromise.all(tasks)and before the firstwriteOneFilein that block, so both the schema file and the failed-assets file land in an existing directory.